# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: chensili

import datetime
import logging
import time
import traceback
from typing import List, Optional, Tuple

import pandas as pd

import coin.proto.coin_request_pb2 as cr_pb2
import coin.proto.options_pricing_pb2 as op_pb2
import coin.support.accounting.logic.estimate_util as estimate_util
from coin.exchange.kr_rest.product.product_impl import generate_product_from_str2
from coin.proto.coin_market_query_pb2 import ExchangeTicker, ProductTicker
from coin.proto.coin_query_pb2 import (
    AccountPositionEstimate, CurrencyBalance, EstimateValue, ProductPosition)
from coin.proto.coin_request_pb2 import AccountRequestProto
from coin.support.accounting.logic.constants import TICKER_CANDIDATES_MEA
from coin.support.options_pricing.types import PricingParamsType
from coin.support.options_pricing.util.pricing_util import calculate_sabr_greeks
from coin.support.options_pricing.util.query_util import query_options_pricing_params
from coin.support.pta.logic.pta_stats_calculator import (
    is_inverse,
    get_product_multiplier,
    get_accounting_currency)
from xunkemgmt_client.client.api_client import XunkemgmtClient
from xunkemgmt_client.support.accounting.database.db_output import DBOutputEstimate
from xunkemgmt_client.support.accounting.logic.constants import (
    ACCOUNTING_CURRENCY_CANDIDATES)


def calculate_position_value(
    account_request: AccountRequestProto,
    product_position: ProductPosition,
    exchange_tickers: List[ExchangeTicker],
    *,
    options_pricing_params: PricingParamsType,
    current_datetime: Optional[datetime.datetime]=None,
    disable_delta_adjust: bool=False,
  ) -> Tuple[str, float]:
  assert account_request.HasField('market_type')
  assert account_request.HasField('exchange')
  market_type = account_request.market_type
  exchange = account_request.exchange
  symbol = product_position.symbol
  product = generate_product_from_str2(
      market_type, exchange, None, symbol, current_datetime)
  product_info = None
  try:
    product_info = product.product_holder.product_info
  except:
    logging.info('Fail to get PI: %s.%s.%s' % (
        product.market_name, product.exchange_name, product.symbol))
  multplier = get_product_multiplier(market_type, exchange, symbol, current_datetime)
  accounting_currency = get_accounting_currency(
      market_type, exchange, symbol, current_datetime)
  estimate_currency = None
  if is_inverse(market_type, exchange, symbol, current_datetime):
    assert accounting_currency == product.base.currency, (
        accounting_currency, product)
    estimate_currency = product.quote.currency
  else:
    if exchange == 'Deribit':
      assert accounting_currency == product.base.currency, (
          accounting_currency, product)
    else:
      assert accounting_currency == product.quote.currency, (
          accounting_currency, product)
    if exchange == 'Bitmex' and symbol == 'ETH-USD.PERPETUAL':
      estimate_currency = accounting_currency
    elif product_info is not None and product_info.asset != '':
      estimate_currency = product_info.asset
    else:
      estimate_currency = product.base.currency
  assert estimate_currency
  asset_multiplier = 1
  if product_info is not None and pd.notnull(product_info.asset_multiplier):
    asset_multiplier = product_info.asset_multiplier
  if disable_delta_adjust:
    delta = 1
  else:
    delta = calculate_position_delta(
        account_request, product_position, exchange_tickers,
        options_pricing_params=options_pricing_params,
        current_datetime=current_datetime)
  #logging.info('%s, %s' % (product_position.symbol, delta))
  estimate_value = None
  if delta is not None:
    estimate_value = product_position.net_position * multplier * asset_multiplier * delta
  return estimate_currency, estimate_value


def calculate_position_delta(
    account_request: AccountRequestProto,
    product_position: ProductPosition,
    exchange_tickers: List[ExchangeTicker],
    *,
    options_pricing_params: Optional[PricingParamsType]=None,
    current_datetime: Optional[datetime.datetime]=None,
) -> Optional[float]:
  market_type = account_request.market_type
  exchange = account_request.exchange
  symbol = product_position.symbol
  product = generate_product_from_str2(
      market_type, exchange, None, symbol, current_datetime)
  if market_type == 'Options':
    sabr_params = [param.sabr for param in options_pricing_params.get(op_pb2.SABR, [])]
    greeks = calculate_sabr_greeks(
        product, exchange_tickers, sabr_params, current_datetime)
    delta = greeks.delta
  else:
    delta = 1
  return delta


def calculate_position_estimate(request: AccountRequestProto,
                                accounting_currency: str,
                                product_position: ProductPosition,
                                exchange_tickers: List[ExchangeTicker],
                                options_pricing_params: PricingParamsType,
) -> EstimateValue:
  estimate_currency, estimate_value = calculate_position_value(
      request, product_position, exchange_tickers,
      options_pricing_params=options_pricing_params)
  position_estimate_value = EstimateValue()
  if estimate_value is not None:
    asset_value = CurrencyBalance(
        currency=estimate_currency.upper(), total=estimate_value)
    position_estimate_value = estimate_util.calculate_asset_value(
        request, asset_value, exchange_tickers, accounting_currency)
  return position_estimate_value


def generate_position_estimate_history_result(
      request_list: List[AccountRequestProto],
      account_info_querier):
  response_list = []
  position_list = account_info_querier.query_account_position_history(request_list)
  ticker_reqs = [cr_pb2.ExchangeApiRequestProto(
        market_type=elem[0], exchange=elem[1], api_version=elem[2])
        for elem in TICKER_CANDIDATES_MEA]
  ticker_rsps = []
  options_pricing_params: PricingParamsType = {}
  with XunkemgmtClient() as client:
    for req in ticker_reqs:
      try:
        rsp = client.query_exchange_tickers(req)
        ticker_rsps.append(rsp)
      except Exception:
        logging.error(traceback.format_exc())
    try:
      options_pricing_params = query_options_pricing_params(models=[op_pb2.SABR])
    except Exception:
      logging.error(traceback.format_exc())

  assert len(request_list) == len(position_list), (len(request_list), len(position_list))
  for request, db_output in zip(request_list, position_list):
    result = None
    query_time = time.time()
    try:
      position = db_output.position
      acct_total_value = None
      acct_total_value_in_usd = None
      accounting_currency = ACCOUNTING_CURRENCY_CANDIDATES[
          (request.market_type, request.exchange)][0]
      estimate_detail = []
      for each_position in position.each_position:
        each_estimate_value = calculate_position_estimate(
              request, accounting_currency, each_position, ticker_rsps,
              options_pricing_params)
        if acct_total_value is None:
          acct_total_value = each_estimate_value.total_value
        else:
          acct_total_value += each_estimate_value.total_value
        if each_estimate_value.total_value_in_usd is not None:
          if acct_total_value_in_usd is None:
            acct_total_value_in_usd = each_estimate_value.total_value_in_usd
          else:
            acct_total_value_in_usd += each_estimate_value.total_value_in_usd
        each_estimate_value.symbol = each_position.symbol
        each_estimate_value.accounting_currency = accounting_currency
        estimate_detail.append(each_estimate_value)
      estimate_value = EstimateValue(
          accounting_currency=accounting_currency,
          total_value=acct_total_value,
          total_value_in_usd=acct_total_value_in_usd)
      account_request = AccountRequestProto(
          market_type=request.market_type,
          exchange=request.exchange,
          owner=request.owner)
      position_estimate = AccountPositionEstimate(
          account_request=account_request,
          query_ts=int(query_time * 10**9),
          position=position,
          each_ticker=ticker_rsps,
          estimate=estimate_value,
          estimate_detail=estimate_detail)
      result = DBOutputEstimate(account_id=request.account_id,
                                query_timestamp=query_time,
                                position_estimate=position_estimate,
                                position_hid=db_output.position_hid)
    except Exception as e:
      logging.error('Fail to generate position estimate.', request)
      logging.error(traceback.format_exc())
    finally:
      response_list.append(result)
  assert len(request_list) == len(response_list)
  return response_list
